前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础篇

    • 环境搭建
    • node部署
    • 基础应用
  • 内置模块

    • 1.0 本地路径处理 path
    • 2.0 文件系统操作 fs
    • 3.1 基础调试 console
    • 3.2 本地调试远程服务器上的Node代码
    • 4.1 网络服务 http
    • 4.2 网络服务 http res
    • 4.3 网络服务 http req
    • 4.4 网络服务 http server
    • 4.5 网络服务 http client
    • 4.6 网络服务 https
    • 4.7 网络TCP net
    • 4.8 网络UDP dgram
    • 4.9 域名解析 dns
    • 5.0 网络地址解析 url
    • 5.1 URL查询字符串 querystring
    • 6.1 流操作 stream
    • 6.2 逐行读取 readline
    • 7.1 进程相关 process
    • 7.2 子进程 child
    • 8.1 二进制数据 buffer
    • 8.2 二进制解码 string_decoder
    • 9.1 事件机制 events
    • 9.2 实用工具模块 util
    • 9.3 数据加密 crypto
    • 9.4 MD5入门介绍及crypto模块的应用
    • 9.4 资源压缩 zlib
    • 9.5 集群 cluster
    • 9.6 v8
  • 进阶篇

    • 5分钟入门非对称加密用法
    • cluster
    • cookie_parser深入
    • crypto模块之理论篇
    • express+cookie_parser签名机制深入剖析
    • express+session实现简易身份认证
    • https
    • log4js入门实例
    • node8_napi
    • 使用 async 控制并发
    • 使用 eventproxy 控制并发
    • 使用 superagent 与 cheerio 完成简单爬虫
    • 基于express+muter的文件上传
    • 将图片转成datauri嵌入到html
    • 常用中间件 body_parser 实现解析
    • 日志模块morgan
    • 服务端字符编解码&乱码处理 charset_enc_dec
    • 测试用例mocha,should,istanbul
    • 调试日志打印debug模块
  • Express

    • 1.1 Express概览篇
    • 1.2 Express
  • Koa2

    • 1.0 koa2概览篇
    • 1.1 快速开始
    • 1.2 async await使用
    • 1.3 koa2简析结构
    • 1.4 koa中间件开发与使用
    • 10.1 单元测试
    • 11.1 开发debug
    • 12.1 快速启动
    • 12.2 框架设计
    • 12.3 分层操作
    • 12.4 数据库设计
    • 12.5 路由设计
    • 12.6 webpack4环境搭建
    • 12.7 使用react
    • 12.8 登录注册功能实现
    • 12.9 session登录态判断处理
    • 13.1 import export使用
    • 2.1 原生koa2实现路由
    • 2.2 koa router中间
    • 3.1 GET请求数据获取
    • 3.2 POST请求数据获取
    • 3.3 koa bodyparser中间件
    • 4.1 原生koa2实现静态资源服务器
    • 4.2 koa static中间件
    • 5.1 koa2使用cookie
    • 5.2 koa2实现session
    • 6.1 koa2加载模板引擎
    • 6.2 ejs模板引擎
    • 7.1 busboy模块
    • 7.2 上传文件简单实现
    • 7.3 异步上传图片实现
    • 8.1 mysql模块
    • 8.2 async await封装使用mysql
    • 8.3 项目建表初始化
    • 9.1 原生koa2实现JSONP
    • 9.2 koa jsonp中间件
  • 其他

    • 操作数据库
    • Session 与 Token
    • Cookie、Session、Token、JWT
    • Socket

# 使用 superagent 与 cheerio 完成简单爬虫

30 秒速记

  • 分工:superagent 负责把网页 HTML 拿回来,cheerio 负责在服务端用 jQuery 写法从 HTML 里挑数据
  • cheerio 只解析字符串,不发请求、不执行页面里的 JS,靠前端渲染出来的内容它看不到
  • 「请求成功」不等于「抓到了」:选择器一个都没命中时返回空数组,要当失败处理
  • 请求要设超时、带 User-Agent,控制频率,遵守 robots.txt 和站点条款

一个简单爬虫就是两步:用 superagent 发 GET 拿到网页源码,再把源码交给 cheerio.load(),像写 jQuery 一样用选择器把要的字段挑出来。 可以把 cheerio 理解成「没有浏览器的 jQuery」,它只认你给它的那段 HTML 字符串,所以页面里靠 JS 渲染出来的内容它是拿不到的,那种情况得找接口或者上 Puppeteer。我写爬虫最在意的是别出现假成功:页面改版之后选择器不命中,接口照样返回 200 和一个空数组,没人会发现,所以结果为空要打日志或者直接报错。

理解「使用 superagent 与 cheerio 完成简单爬虫」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

面试官追问

追问 1cheerio.load(html) 之后选择器明明在浏览器里能选中,代码里却拿到空数组,为什么?
参考回答

大概率那块内容是前端 JS 渲染的,superagent 拿到的源码里根本没有。在浏览器里「查看网页源代码」(不是 Elements 面板)搜一下就知道。这种页面去抓它背后的数据接口,实在不行再用 Puppeteer。

追问 2上游偶尔返回一个验证页或空页面,接口还是 200 加空数组,怎么让它暴露出来?
参考回答

把「抓到了数据」算进成功条件:items.length === 0 时记录上游状态码和页面标题,返回错误而不是空数组。命中数量做个监控,突然掉到 0 基本就是改版或者被拦了。

追问 3CI 里这个爬虫测试时好时坏,本地一直正常,你会怎么拆?
参考回答

把网络和解析拆开测。解析部分存一份真实的 HTML 样本当测试数据,断言能解析出几条、字段齐不齐,这部分完全离线、结果稳定;真实请求只留一个冒烟用例,别让外网抖动决定 CI 是否通过。

追问 4请求一直不返回,进程挂在那里不退出,先加什么?
参考回答

先加超时:superagent.get(url).timeout({ response: 5000, deadline: 10000 })。默认没有超时,对方不回包你就一直等。然后确认失败回调里有 return next(err) 或者显式退出码,别吞错。

追问 5爬别人网站,有哪些红线?
参考回答

看 robots.txt 和站点条款,不绕登录、不破验证码,请求加间隔、带可识别的 User-Agent,抓过的页面缓存起来别重复打。能用官方 API 就别爬页面。

# 《使用 superagent 与 cheerio 完成简单爬虫》

# 目标

30 秒速记

  • 需求:访问 localhost:3000/,实时抓 CNode 首页,返回 [{ title, href }] 的 JSON
  • 链路:Express 路由 → superagent 请求首页 → cheerio 遍历 #topic_list .topic_title → res.send(items)
  • href 页面上是相对路径 /topic/xxx,要补全成绝对地址:new URL(href, base).href
  • 上游挂了或选择器没命中,要返回错误,别默默给个空数组

这个练习就是做一个「代理接口」:浏览器请求你的 3000 端口,你去抓 CNode 首页,把帖子标题和链接整理成 JSON 数组还回去。 调用方拿到的是干净的结构化数据,不用自己再去解析 HTML,这其实就是很多数据中转服务的原型。实现上要注意两个细节:页面上的链接是 /topic/xxx 这种相对路径,要用 new URL(href, 'https://cnodejs.org/') 补全;上游请求失败要走 next(err),不能把空数组当成功返回。

建立一个 lesson3 项目,在其中编写代码。

当在浏览器中访问 http://localhost:3000/ 时,输出 CNode( https://cnodejs.org/ ) 社区首页的所有帖子标题和链接,以 json 的形式。

输出示例:

[
  {
    "title": "【公告】发招聘帖的同学留意一下这里",
    "href": "http://cnodejs.org/topic/541ed2d05e28155f24676a12"
  },
  {
    "title": "发布一款 Sublime Text 下的 JavaScript 语法高亮插件",
    "href": "http://cnodejs.org/topic/54207e2efffeb6de3d61f68f"
  }
]

@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1返回的数组里只有 title,没有 href,算完成吗?
参考回答

不算,约定的结构是每条都有 title 和 href。通常是只取了 .text() 忘了 $el.attr('href'),调用方拿到没链接的标题基本没法用。

追问 2href 有的是 /topic/...,有的已经是完整地址,怎么统一?
参考回答

直接用 new URL(href, 'https://cnodejs.org/').href,相对路径会被补全,绝对地址原样保留。别用字符串拼,容易拼出双斜杠或者重复域名;老写法 url.resolve 已经被标记为过时了。

追问 3接口偶尔返回空数组,但浏览器打开 CNode 明明有帖子,先查哪?
参考回答

先打日志看上游返回的状态码和 HTML 长度,确认拿到的是不是正常首页。拿到的是正常首页就去查选择器;如果是限流页或者错误页,问题在请求那一侧。

追问 4每次访问都去抓一遍上游,上游慢或者挂了会怎样?怎么改?
参考回答

你的接口跟着慢、跟着挂。加一层短缓存,比如结果缓存 60 秒,上游失败时返回上一份缓存并标记过期,既减轻对方压力也提高自己的可用性,代价是数据不再是实时的。

# 挑战

30 秒速记

  • 在 title、href 之外再返回 author
  • 遍历单位要换成「整条帖子」的容器(#topic_list .cell),在同一个容器里分别找标题和作者
  • 作者在头像链接上:$cell.find('.user_avatar img').attr('title'),或者从头像 href 的 /user/xxx 里截出来
  • 别分别选出三组节点再按下标拼,中间多一个置顶或广告就全错位

加 author 字段的关键不是多写一个选择器,而是把遍历单位从「标题链接」换成「一整条帖子」,在同一个容器里把三个字段一起取出来。 如果还是只遍历 .topic_title,作者根本不在这个节点里面;如果另外再选一组作者节点按下标对应,只要列表里混进一条置顶或者没头像的帖子,作者就整体错位了。具体选择器以当时的页面为准,CNode 首页的作者名在头像 img 的 title 属性上。

$('#topic_list .cell').each((i, el) => {
  const $cell = $(el)
  const $title = $cell.find('.topic_title')
  items.push({
    title: $title.attr('title'),
    href: new URL($title.attr('href'), 'https://cnodejs.org/').href,
    author: $cell.find('.user_avatar img').attr('title'),
  })
})
@前端进阶之旅: 代码已经复制到剪贴板

访问 http://localhost:3000/ 时,输出包括主题的作者,

示例:

[
  {
    "title": "【公告】发招聘帖的同学留意一下这里",
    "href": "http://cnodejs.org/topic/541ed2d05e28155f24676a12",
    "author": "alsotang"
  },
  {
    "title": "发布一款 Sublime Text 下的 JavaScript 语法高亮插件",
    "href": "http://cnodejs.org/topic/54207e2efffeb6de3d61f68f",
    "author": "otheruser"
  }
]
@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1有人直接写 $element.attr('author'),结果拿到什么?
参考回答

undefined,.topic_title 这个 a 标签上根本没有 author 属性。而且 res.send 序列化 JSON 时 undefined 的字段会被直接丢掉,前端看到的是这一项压根没有 author。

追问 2标题、作者各选一组再按下标拼起来,为什么不靠谱?
参考回答

基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础篇

    • 环境搭建
    • node部署
    • 基础应用
  • 内置模块

    • 1.0 本地路径处理 path
    • 2.0 文件系统操作 fs
    • 3.1 基础调试 console
    • 3.2 本地调试远程服务器上的Node代码
    • 4.1 网络服务 http
    • 4.2 网络服务 http res
    • 4.3 网络服务 http req
    • 4.4 网络服务 http server
    • 4.5 网络服务 http client
    • 4.6 网络服务 https
    • 4.7 网络TCP net
    • 4.8 网络UDP dgram
    • 4.9 域名解析 dns
    • 5.0 网络地址解析 url
    • 5.1 URL查询字符串 querystring
    • 6.1 流操作 stream
    • 6.2 逐行读取 readline
    • 7.1 进程相关 process
    • 7.2 子进程 child
    • 8.1 二进制数据 buffer
    • 8.2 二进制解码 string_decoder
    • 9.1 事件机制 events
    • 9.2 实用工具模块 util
    • 9.3 数据加密 crypto
    • 9.4 MD5入门介绍及crypto模块的应用
    • 9.4 资源压缩 zlib
    • 9.5 集群 cluster
    • 9.6 v8
  • 进阶篇

    • 5分钟入门非对称加密用法
    • cluster
    • cookie_parser深入
    • crypto模块之理论篇
    • express+cookie_parser签名机制深入剖析
    • express+session实现简易身份认证
    • https
    • log4js入门实例
    • node8_napi
    • 使用 async 控制并发
    • 使用 eventproxy 控制并发
    • 使用 superagent 与 cheerio 完成简单爬虫
    • 基于express+muter的文件上传
    • 将图片转成datauri嵌入到html
    • 常用中间件 body_parser 实现解析
    • 日志模块morgan
    • 服务端字符编解码&乱码处理 charset_enc_dec
    • 测试用例mocha,should,istanbul
    • 调试日志打印debug模块
  • Express

    • 1.1 Express概览篇
    • 1.2 Express
  • Koa2

    • 1.0 koa2概览篇
    • 1.1 快速开始
    • 1.2 async await使用
    • 1.3 koa2简析结构
    • 1.4 koa中间件开发与使用
    • 10.1 单元测试
    • 11.1 开发debug
    • 12.1 快速启动
    • 12.2 框架设计
    • 12.3 分层操作
    • 12.4 数据库设计
    • 12.5 路由设计
    • 12.6 webpack4环境搭建
    • 12.7 使用react
    • 12.8 登录注册功能实现
    • 12.9 session登录态判断处理
    • 13.1 import export使用
    • 2.1 原生koa2实现路由
    • 2.2 koa router中间
    • 3.1 GET请求数据获取
    • 3.2 POST请求数据获取
    • 3.3 koa bodyparser中间件
    • 4.1 原生koa2实现静态资源服务器
    • 4.2 koa static中间件
    • 5.1 koa2使用cookie
    • 5.2 koa2实现session
    • 6.1 koa2加载模板引擎
    • 6.2 ejs模板引擎
    • 7.1 busboy模块
    • 7.2 上传文件简单实现
    • 7.3 异步上传图片实现
    • 8.1 mysql模块
    • 8.2 async await封装使用mysql
    • 8.3 项目建表初始化
    • 9.1 原生koa2实现JSONP
    • 9.2 koa jsonp中间件
  • 其他

    • 操作数据库
    • Session 与 Token
    • Cookie、Session、Token、JWT
    • Socket
完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

使用 superagent 与 cheerio 完成简单爬虫|Nodejs专题系列

# 使用 superagent 与 cheerio 完成简单爬虫

30 秒速记

  • 分工:superagent 负责把网页 HTML 拿回来,cheerio 负责在服务端用 jQuery 写法从 HTML 里挑数据
  • cheerio 只解析字符串,不发请求、不执行页面里的 JS,靠前端渲染出来的内容它看不到
  • 「请求成功」不等于「抓到了」:选择器一个都没命中时返回空数组,要当失败处理
  • 请求要设超时、带 User-Agent,控制频率,遵守 robots.txt 和站点条款

一个简单爬虫就是两步:用 superagent 发 GET 拿到网页源码,再把源码交给 cheerio.load(),像写 jQuery 一样用选择器把要的字段挑出来。 可以把 cheerio 理解成「没有浏览器的 jQuery」,它只认你给它的那段 HTML 字符串,所以页面里靠 JS 渲染出来的内容它是拿不到的,那种情况得找接口或者上 Puppeteer。我写爬虫最在意的是别出现假成功:页面改版之后选择器不命中,接口照样返回 200 和一个空数组,没人会发现,所以结果为空要打日志或者直接报错。

理解「使用 superagent 与 cheerio 完成简单爬虫」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

面试官追问

追问 1cheerio.load(html) 之后选择器明明在浏览器里能选中,代码里却拿到空数组,为什么?
参考回答

大概率那块内容是前端 JS 渲染的,superagent 拿到的源码里根本没有。在浏览器里「查看网页源代码」(不是 Elements 面板)搜一下就知道。这种页面去抓它背后的数据接口,实在不行再用 Puppeteer。

追问 2上游偶尔返回一个验证页或空页面,接口还是 200 加空数组,怎么让它暴露出来?
参考回答

把「抓到了数据」算进成功条件:items.length === 0 时记录上游状态码和页面标题,返回错误而不是空数组。命中数量做个监控,突然掉到 0 基本就是改版或者被拦了。

追问 3CI 里这个爬虫测试时好时坏,本地一直正常,你会怎么拆?
参考回答

把网络和解析拆开测。解析部分存一份真实的 HTML 样本当测试数据,断言能解析出几条、字段齐不齐,这部分完全离线、结果稳定;真实请求只留一个冒烟用例,别让外网抖动决定 CI 是否通过。

追问 4请求一直不返回,进程挂在那里不退出,先加什么?
参考回答

先加超时:superagent.get(url).timeout({ response: 5000, deadline: 10000 })。默认没有超时,对方不回包你就一直等。然后确认失败回调里有 return next(err) 或者显式退出码,别吞错。

追问 5爬别人网站,有哪些红线?
参考回答

看 robots.txt 和站点条款,不绕登录、不破验证码,请求加间隔、带可识别的 User-Agent,抓过的页面缓存起来别重复打。能用官方 API 就别爬页面。

# 《使用 superagent 与 cheerio 完成简单爬虫》

# 目标

30 秒速记

  • 需求:访问 localhost:3000/,实时抓 CNode 首页,返回 [{ title, href }] 的 JSON
  • 链路:Express 路由 → superagent 请求首页 → cheerio 遍历 #topic_list .topic_title → res.send(items)
  • href 页面上是相对路径 /topic/xxx,要补全成绝对地址:new URL(href, base).href
  • 上游挂了或选择器没命中,要返回错误,别默默给个空数组

这个练习就是做一个「代理接口」:浏览器请求你的 3000 端口,你去抓 CNode 首页,把帖子标题和链接整理成 JSON 数组还回去。 调用方拿到的是干净的结构化数据,不用自己再去解析 HTML,这其实就是很多数据中转服务的原型。实现上要注意两个细节:页面上的链接是 /topic/xxx 这种相对路径,要用 new URL(href, 'https://cnodejs.org/') 补全;上游请求失败要走 next(err),不能把空数组当成功返回。

建立一个 lesson3 项目,在其中编写代码。

当在浏览器中访问 http://localhost:3000/ 时,输出 CNode( https://cnodejs.org/ ) 社区首页的所有帖子标题和链接,以 json 的形式。

输出示例:

[
  {
    "title": "【公告】发招聘帖的同学留意一下这里",
    "href": "http://cnodejs.org/topic/541ed2d05e28155f24676a12"
  },
  {
    "title": "发布一款 Sublime Text 下的 JavaScript 语法高亮插件",
    "href": "http://cnodejs.org/topic/54207e2efffeb6de3d61f68f"
  }
]

@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1返回的数组里只有 title,没有 href,算完成吗?
参考回答

不算,约定的结构是每条都有 title 和 href。通常是只取了 .text() 忘了 $el.attr('href'),调用方拿到没链接的标题基本没法用。

追问 2href 有的是 /topic/...,有的已经是完整地址,怎么统一?
参考回答

直接用 new URL(href, 'https://cnodejs.org/').href,相对路径会被补全,绝对地址原样保留。别用字符串拼,容易拼出双斜杠或者重复域名;老写法 url.resolve 已经被标记为过时了。

追问 3接口偶尔返回空数组,但浏览器打开 CNode 明明有帖子,先查哪?
参考回答

先打日志看上游返回的状态码和 HTML 长度,确认拿到的是不是正常首页。拿到的是正常首页就去查选择器;如果是限流页或者错误页,问题在请求那一侧。

追问 4每次访问都去抓一遍上游,上游慢或者挂了会怎样?怎么改?
参考回答

你的接口跟着慢、跟着挂。加一层短缓存,比如结果缓存 60 秒,上游失败时返回上一份缓存并标记过期,既减轻对方压力也提高自己的可用性,代价是数据不再是实时的。

# 挑战

30 秒速记

  • 在 title、href 之外再返回 author
  • 遍历单位要换成「整条帖子」的容器(#topic_list .cell),在同一个容器里分别找标题和作者
  • 作者在头像链接上:$cell.find('.user_avatar img').attr('title'),或者从头像 href 的 /user/xxx 里截出来
  • 别分别选出三组节点再按下标拼,中间多一个置顶或广告就全错位

加 author 字段的关键不是多写一个选择器,而是把遍历单位从「标题链接」换成「一整条帖子」,在同一个容器里把三个字段一起取出来。 如果还是只遍历 .topic_title,作者根本不在这个节点里面;如果另外再选一组作者节点按下标对应,只要列表里混进一条置顶或者没头像的帖子,作者就整体错位了。具体选择器以当时的页面为准,CNode 首页的作者名在头像 img 的 title 属性上。

$('#topic_list .cell').each((i, el) => {
  const $cell = $(el)
  const $title = $cell.find('.topic_title')
  items.push({
    title: $title.attr('title'),
    href: new URL($title.attr('href'), 'https://cnodejs.org/').href,
    author: $cell.find('.user_avatar img').attr('title'),
  })
})
@前端进阶之旅: 代码已经复制到剪贴板

访问 http://localhost:3000/ 时,输出包括主题的作者,

示例:

[
  {
    "title": "【公告】发招聘帖的同学留意一下这里",
    "href": "http://cnodejs.org/topic/541ed2d05e28155f24676a12",
    "author": "alsotang"
  },
  {
    "title": "发布一款 Sublime Text 下的 JavaScript 语法高亮插件",
    "href": "http://cnodejs.org/topic/54207e2efffeb6de3d61f68f",
    "author": "otheruser"
  }
]
@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1有人直接写 $element.attr('author'),结果拿到什么?
参考回答

undefined,.topic_title 这个 a 标签上根本没有 author 属性。而且 res.send 序列化 JSON 时 undefined 的字段会被直接丢掉,前端看到的是这一项压根没有 author。

追问 2标题、作者各选一组再按下标拼起来,为什么不靠谱?
参考回答

两组节点数量一旦不一致就错位,比如某条帖子作者被删了没头像,后面每一条的作者都会往前挪一位,而且不报错。以帖子容器为单位取,最多是某一条 author 为空,不会连累别的。

追问 3线上 author 突然全没了,同事怀疑 JSON 序列化有问题,你怎么看?
参考回答

这个锅基本不在序列化。先把抓到的 HTML 存下来,看头像节点还在不在、属性名有没有变,再在本地用 cheerio 跑一下选择器,看命中数量。页面改版是爬虫最常见的挂法。

追问 4作者信息改从站点 API 拿行不行?
参考回答

如果站点有公开 API,生产上我会优先用,字段稳定、不怕改版,CNode 就有 /api/v1/topics。这个练习的目的是练选择器,所以还是按页面抓。

# 知识点

30 秒速记

  • superagent:发请求的,链式调用,.get(url).set(header).timeout(ms).end(cb),也能直接 await
  • cheerio:解析 HTML 的,cheerio.load(html) 返回一个 $,后面就是 jQuery 那套 find / attr / text / each
  • 两者衔接点:响应的 res.text(HTML 源码字符串)
  • cheerio 不跑 JS、没有布局,速度快但看不到前端渲染的内容

这节就两个知识点:superagent 负责把网页拿回来,cheerio 负责把网页拆开。 中间靠 res.text 接起来,它就是网页源码字符串。cheerio 可以当成跑在 Node.js 里的 jQuery,没有浏览器、不执行脚本,所以很快,但只能处理服务端直接吐出来的 HTML。superagent 的链式写法很顺手,现在也可以直接 await,不一定非要 .end 回调。

const superagent = require('superagent')
const cheerio = require('cheerio')

const res = await superagent.get('https://cnodejs.org/').timeout(5000)
const $ = cheerio.load(res.text)
console.log($('#topic_list .topic_title').length) // 首页帖子数,比如 40
@前端进阶之旅: 代码已经复制到剪贴板

← 使用 eventproxy 控制并发基于express+muter的文件上传 →

fe